package com.nightpiter.view;

import static com.nightpiter.controller.log.Logger.log;

import java.util.List;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.graphics.PointF;
import android.os.Handler;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import android.widget.ZoomDialog;
import android.widget.ZoomSlider;

import com.nightpiter.NightPiter;
import com.nightpiter.R;
import com.nightpiter.controller.MetroController;
import com.nightpiter.controller.algorithms.MetroRouterState;
import com.nightpiter.controller.algorithms.Route;
import com.nightpiter.model.MetroModel;
import com.nightpiter.model.MetroModelState;
import com.nightpiter.model.objects.Station;
import com.nightpiter.view.dialogs.LoaderDialogContent;
import com.nightpiter.view.dialogs.RouteInfoDialog;

/**
 * MVC's View class
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class MetroView implements MetroScreen.Subscriber,
		ZoomSlider.OnZoomChangedListener, DialogInterface.OnClickListener,
		RouteInfoDialog.Subscriber, AdapterView.OnItemClickListener {

	private static final int PAD_SCROLL_STEP = 7;

	private static final int LONG_TOUCH_DURATION = 700;

	private static final int MIN_ZOOM = 0;

	private static final int MAX_ZOOM = 25;

	/**
	 * Application context
	 */
	private NightPiter context;

	/**
	 * SurfaceView instance (surface of the surfaceHolder)
	 */
	private MetroScreen mapScreenView;

	/**
	 * zooming slider
	 */
	private ZoomDialog zoomDialog;

	/**
	 * metro loader content view
	 */
	private LoaderDialogContent loaderDialogContent;

	/**
	 * about application dialog view
	 */
	private View aboutDialogContent;

	/**
	 * dialog to show station info
	 */
	private RouteInfoDialog routeInfoDialog;

	/**
	 * widget for the loader
	 */
	private AlertDialog loaderDialog;

	/**
	 * alert builder
	 */
	private AlertDialog.Builder loaderDialogBuilder;

	/**
	 * widget for the openfile dialog
	 */
	private AlertDialog openFileDialog;

	/**
	 * alert builder
	 */
	private AlertDialog.Builder openFileDialogBuilder;

	/**
	 * widget for the about dialog
	 */
	private AlertDialog aboutDialog;

	/**
	 * alert builder
	 */
	private AlertDialog.Builder aboutDialogBuilder;

	/**
	 * listView containing available files
	 */
	private ListView filesListView;

	/**
	 * a list of filenames
	 */
	private List<String> filesList;

	/**
	 * short message to show if some function is not yet implemented
	 */
	@SuppressWarnings("unused")
	private Toast notYetImplementedToast;

	/**
	 * do not show loader dialog after user pressed hide button
	 */
	private boolean preventLoaderPopup;

	/**
	 * indicates the last action type if was click
	 */
	private boolean wasTouchDown = false;

	/**
	 * indicates the last action was moving
	 */
	private boolean wasTouchMove = false;

	/**
	 * last touched point coordinates
	 */
	private float lastX = 0, lastY = 0;

	/**
	 * station found after last touch
	 */
	private Station pendingStationProcessing = null;

	/**
	 * indicates if the long touch timer is active
	 */
	private boolean pendingLongTouchAction = false;

	/**
	 * indicates if the station was found
	 */
	private boolean stationRetrieved = false;

	/**
	 * current thread handler
	 */
	Handler handler = new Handler();

	/**
	 * this will show zoomDialog on the screen
	 */
	private Runnable onLongTouchRunnable = new Runnable() {
		public void run() {
			onLongTouchEvent();
		}
	};

	/**
	 * this will redraw the map only after a bit delay
	 */
	private Runnable mapDrawDelaedRunnable = new Runnable() {
		public void run() {
			mapScreenView.drawMetro();
		}
	};

	/**
	 * shows if zoom has changed since last map rendering
	 */
	private boolean zoomChanged = false;

	/**
	 * default constructor
	 * 
	 * @param context
	 *            the application context
	 */
	public MetroView(NightPiter context) {
		this.context = context;
		log("View instance created");
	}

	/**
	 * should be called after another MVC components created
	 */
	public void init() {
		log("View init() call");

		this.context.requestWindowFeature(Window.FEATURE_PROGRESS);
		this.context.setContentView(R.layout.main);

		this.context.setProgressBarVisibility(false);
		this.context.setProgress(0);
		this.context.setProgressBarIndeterminate(false);

		this.loaderDialogContent = (LoaderDialogContent) this.context
				.getViewInflate().inflate(R.layout.loader, null, null);

		this.aboutDialogContent = (View) this.context.getViewInflate().inflate(
				R.layout.about, null, null);

		this.routeInfoDialog = (RouteInfoDialog) context
				.findViewById(R.id.info_dialog);
		this.routeInfoDialog.subscribe(this);

		this.mapScreenView = (MetroScreen) context.findViewById(R.id.metro);
		this.mapScreenView.subscribe(this);

		this.zoomDialog = new ZoomDialog(context);
		this.zoomDialog.setParams(MIN_ZOOM, MAX_ZOOM, MIN_ZOOM, this, true);

		this.notYetImplementedToast = Toast.makeText(context,
				"this function is currently disabled", Toast.LENGTH_LONG);

		this.filesListView = new ListView(context);
		this.filesListView.setOnItemClickListener(this);

		this.openFileDialogBuilder = new AlertDialog.Builder(context);
		this.openFileDialogBuilder.setTitle(R.string.open_title);
		this.openFileDialogBuilder.setIcon(R.drawable.openmap_small);
		this.openFileDialogBuilder.setView(this.filesListView);
		this.openFileDialogBuilder.setNegativeButton("Cancel", this);

		this.aboutDialogBuilder = new AlertDialog.Builder(context);
		this.aboutDialogBuilder.setTitle(R.string.about_title);
		this.aboutDialogBuilder.setIcon(R.drawable.matmex_small);
		this.aboutDialogBuilder.setView(this.aboutDialogContent);
		this.aboutDialogBuilder.setPositiveButton("Hide", this);
		this.aboutDialogBuilder.setNegativeButton("Map info", this);

		this.loaderDialogBuilder = new AlertDialog.Builder(context);
		this.loaderDialogBuilder.setTitle(R.string.loader_title);
		this.loaderDialogBuilder.setView(this.loaderDialogContent);
		this.loaderDialogBuilder.setPositiveButton("Hide", this);

		this.loaderDialog = loaderDialogBuilder.create();

		this.preventLoaderPopup = false;

	}

	/**
	 * set the application's title bar text
	 * 
	 * @param action
	 */
	public void setApplicationTitle(String action) {
		MetroModel model = getModel();
		String title;
		String appName = getContext().getString(R.string.app_name);
		if (action == null) {
			this.context.setProgressBarVisibility(false);
			if (model.getCityName().length() != 0) {
				title = appName + " - " + model.getCityName();
			} else {
				title = appName;
			}
		} else {
			this.context.setProgressBarVisibility(true);
			title = appName + " :: " + action;
		}
		getContext().setTitle(title);
	}

	/**
	 * called when open new map menu item selected
	 */
	public void onOpenFileMenuItemSelected() {
		filesList = getController().getFilesList();
		if (filesList.isEmpty()) {
			Toast.makeText(context, "there are no maps in the directory",
					Toast.LENGTH_LONG).show();
		} else {
			filesListView.setAdapter(new ArrayAdapter<String>(context,
					R.layout.file_row, filesList));
			openFileDialog = openFileDialogBuilder.show();
		}
	}

	/**
	 * user selected a file from the list
	 * 
	 * @param parent
	 * @param v
	 * @param position
	 * @param id
	 */
	public void onItemClick(AdapterView parent, View v, int position, long id) {
		if (parent == this.filesListView) {
			this.routeInfoDialog.hide();
			getController().loadModel(
					context.getString(R.string.maps_dir) + "/"
							+ filesList.get(position));
			this.openFileDialog.dismiss();
		}
	}

	/**
	 * this would be called when the model changed
	 */
	public void onModelStateChanged(MetroModelState state) {
		MetroModelState currentState = getModel().getState();
		if (state.getAction() != MetroModelState.Action.IDLE) {
			this.routeInfoDialog.hide();
			if (!this.preventLoaderPopup) {
				this.loaderDialog.show();
				this.loaderDialogContent.showState(state);
			}
			if (state.getAction() == MetroModelState.Action.BUILDING) {
				setApplicationTitle("Building the Model");
			} else if (state.getAction() == MetroModelState.Action.READING) {
				setApplicationTitle("Reading file");
			}
		} else if (currentState.isReady()) {
			// TODO unexpected behavior if state is too out-of-date
			setApplicationTitle("Preparing to draw");
			this.loaderDialogContent.showDrawerPrepare(false);
			this.mapScreenView.setModel(getModel());
			this.zoomDialog.setCurrent(0);
		}
	}

	/**
	 * metro screen completed drawer preparing, so close loaderDialog
	 */
	public void onDrawerPrepareComplete() {
		this.loaderDialogContent.showDrawerPrepare(true);
		// timeout
		(new Handler()).postDelayed(new Runnable() {
			public void run() {
				preventLoaderPopup = false;
				loaderDialog.dismiss();
				loaderDialog = loaderDialogBuilder.create();
			}
		}, 1400);
		setApplicationTitle(null);
	}

	/**
	 * called when user pressed a button on some Dialog
	 */
	public void onClick(DialogInterface dialog, int button) {
		if (dialog == this.loaderDialog) {
			switch (button) {
			case DialogInterface.BUTTON1:
				this.preventLoaderPopup = true;
				break;

			default:
				break;
			}
			this.loaderDialog = loaderDialogBuilder.create();

		} else if (dialog == this.aboutDialog) {
			switch (button) {
			case DialogInterface.BUTTON2:
				AlertDialog.Builder mapDialogBuilder = new AlertDialog.Builder(
						context);
				mapDialogBuilder.setTitle("Map info");
				mapDialogBuilder.setPositiveButton("Hide", this);
				String message = "";
				if (getModel().getCityName().length() == 0) {
					message += "City name: not presented\n";
				} else {
					message += "City name: " + getModel().getCityName() + "\n";
				}
				if (getModel().getAuthors().size() == 0) {
					message += "Authors: not presented\n";
				} else {
					message += "Authors:\n";
					for (String author : getModel().getAuthors()) {
						message += "    " + author + "\n";
					}
				}
				message += "\nthe Model statistics:\n";
				message += "   stations: " + getModel().getStations().size()
						+ "\n";
				message += "   lines: " + getModel().getLines().size() + "\n";
				message += "   hubs: " + getModel().getHubs().size() + "\n";
				message += "   tunnels: " + getModel().getTunnels().size()
						+ "\n";
				message += "   transfers: " + getModel().getTransfers().size()
						+ "\n";
				message += "\nComment: " + getModel().getComment() + "\n";

				mapDialogBuilder.setMessage(message);

				mapDialogBuilder.show();
				break;

			default:
				break;
			}

		}
	}

	/**
	 * this would fired when the screen touched
	 */
	public void onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {

		case MotionEvent.ACTION_DOWN:
			wasTouchDown = true;
			wasTouchMove = false;
			lastX = event.getX();
			lastY = event.getY();
			pendingStationProcessing = null;
			stationRetrieved = false;
			if (getModel().getState().getAction() == MetroModelState.Action.IDLE) {
				PointF modelPoint = this.mapScreenView.screenToModel(
						(int) lastX, (int) lastY);
				getController().searchStation(modelPoint);
				log("requesting controller to search a station at (" + lastX
						+ "," + lastY + ")");
			}
			pendingLongTouchAction = true;
			handler.removeCallbacks(onLongTouchRunnable);
			handler.postDelayed(onLongTouchRunnable, LONG_TOUCH_DURATION);
			break;

		case MotionEvent.ACTION_MOVE:
			wasTouchMove = true;
			pendingLongTouchAction = false;
			handler.removeCallbacks(onLongTouchRunnable);
			mapScreenView.setOffset((int) (event.getX() - lastX), (int) (event
					.getY() - lastY));
			lastX = event.getX();
			lastY = event.getY();
			break;

		case MotionEvent.ACTION_UP:
			wasTouchDown = false;
			handler.removeCallbacks(onLongTouchRunnable);
			if (!wasTouchMove) {
				log("click!");
				if (stationRetrieved && pendingLongTouchAction) {
					routeInfoDialog.processStation(pendingStationProcessing,
							false);
					pendingStationProcessing = null;
				}
			} else {
				mapScreenView.drawMetro();
			}
			pendingLongTouchAction = false;
			break;
		}
	}

	/**
	 * this would fired when the screen touched for a long time
	 */
	public void onLongTouchEvent() {
		log("long click!");
		pendingLongTouchAction = false;
		if (pendingStationProcessing != null) {
			routeInfoDialog.processStation(pendingStationProcessing, true);
			pendingStationProcessing = null;
		} else {
			zoomDialog
					.setPosition(mapScreenView, (int) lastX, (int) lastY + 22);
			zoomDialog.show();
		}
	}

	/**
	 * fires when controller finished searching station
	 * 
	 * @param station
	 *            found station or null if it's not found
	 */
	public void onStationRetrieved(Station station) {
		stationRetrieved = true;
		if (wasTouchDown) {
			pendingStationProcessing = station;
		} else if (!wasTouchMove) {
			// TODO this should be processed in onTouch or in onLongTouch, not
			// here; the call below may be caused by redrawing freezes
			this.routeInfoDialog.processStation(station, false);
		}
	}

	/**
	 * fires when controller finished retrieving routes
	 * 
	 * @param station
	 *            found station or null if it's not found
	 */
	public void onRoutesRetrieved(Route[] routes) {
		this.routeInfoDialog.processRoutes(routes);
	}

	/**
	 * this will fire when the router would enter a new state
	 */
	public void onRouterStateChanged(MetroRouterState state) {
		this.routeInfoDialog.onRouterStateChanged(state);
		if (state.getAction() != MetroRouterState.Action.IDLE) {
			setApplicationTitle("Searching Routes");
		} else {
			setApplicationTitle(null);
		}
	}

	/**
	 * should be fired from RouterInfoDialog
	 */
	public void onRouteFromDefine(Station fromStation) {
		getController().prepareRouter(fromStation);
	}

	/**
	 * should be fired from RouterInfoDialog
	 */
	public void onRouteToDefine(Station toStation) {
		getController().searchRoutes(toStation);
	}

	/**
	 * RouteRunnable class.
	 * 
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private class RouteRunnable implements Runnable {
		/**
		 * the route to select
		 */
		private Route route;

		/**
		 * @return the route
		 */
		public Route getRoute() {
			return route;
		}

		/**
		 * @param route
		 *            the route to set
		 */
		public void setRoute(Route route) {
			this.route = route;
		}

		public void run() {
			mapScreenView.selectRoute(route.getTunnels(), route.getTransfers(),
					route.getMultiLineStations());
		}
	}

	/**
	 * this will be used to delay the rendering and get freezes out
	 */
	private RouteRunnable selectRouteRunnable = new RouteRunnable();

	/**
	 * should be fired from RouterInfoDialog
	 */
	public void onRouteSelect(Route route) {
		if (route != null) {
			selectRouteRunnable.setRoute(route);
			this.handler.post(selectRouteRunnable);
		} else {
			this.handler.post(new Runnable() {
				public void run() {
					mapScreenView.deselectTransitions();
				}
			});
		}
	}

	/**
	 * should be fired from RouterInfoDialog
	 */
	public void onStationSelect(Station station, boolean scrollToPoint) {
		if (station != null) {
			this.mapScreenView.selectStation(station, scrollToPoint);
		} else {
			this.mapScreenView.deselectStation();
		}
	}

	/**
	 * should be fired from RouterInfoDialog
	 */
	public void onDialogHide() {
	}

	/**
	 * this will called when zoom menu item selected
	 */
	public void onZoomMenuItemSelected() {
		zoomDialog.setPosition(this.mapScreenView, 60, 400);
		zoomDialog.show();
	}

	public void onZoomChanged(ZoomSlider zoomSlider, int oldZoom, int newZoom) {
		this.mapScreenView.setScale(newZoom);
		zoomChanged = true;
	}

	public void onZoomCompleted() {
		if (zoomChanged) {
			zoomChanged = false;
			this.mapScreenView.drawMetro();
		}
	}

	public void onZoomIn(ZoomSlider zoomSlider, int oldZoom, int newZoom) {
		this.mapScreenView.setScale(newZoom);
		handler.removeCallbacks(mapDrawDelaedRunnable);
		handler.postDelayed(mapDrawDelaedRunnable, LONG_TOUCH_DURATION);
	}

	public void onZoomOut(ZoomSlider zoomSlider, int oldZoom, int newZoom) {
		this.mapScreenView.setScale(newZoom);
		handler.removeCallbacks(mapDrawDelaedRunnable);
		handler.postDelayed(mapDrawDelaedRunnable, LONG_TOUCH_DURATION);
	}

	public void onKeyDown(int keyCode, KeyEvent event) {
		switch (keyCode) {
		case KeyEvent.KEYCODE_DPAD_UP:
			this.mapScreenView.setOffset(0, PAD_SCROLL_STEP);
			break;
		case KeyEvent.KEYCODE_DPAD_DOWN:
			this.mapScreenView.setOffset(0, -PAD_SCROLL_STEP);
			break;

		case KeyEvent.KEYCODE_DPAD_LEFT:
			this.mapScreenView.setOffset(PAD_SCROLL_STEP, 0);
			break;

		case KeyEvent.KEYCODE_DPAD_RIGHT:
			this.mapScreenView.setOffset(-PAD_SCROLL_STEP, 0);
			break;

		}
	}

	public void onKeyUp(int keyCode, KeyEvent event) {
		switch (keyCode) {
		case KeyEvent.KEYCODE_DPAD_UP:
		case KeyEvent.KEYCODE_DPAD_DOWN:
		case KeyEvent.KEYCODE_DPAD_LEFT:
		case KeyEvent.KEYCODE_DPAD_RIGHT:

			this.mapScreenView.drawMetro();
			break;

		}
	}

	public void onAboutMenuItemSelected() {
		aboutDialog = aboutDialogBuilder.show();
	}

	/**
	 * @return the context
	 */
	protected NightPiter getContext() {
		return context;
	}

	/**
	 * @return the model
	 * @see com.nightpiter.NightPiter#getModel()
	 */
	protected MetroModel getModel() {
		return context.getModel();
	}

	/**
	 * @return the controller
	 * @see com.nightpiter.NightPiter#getController()
	 */
	protected MetroController getController() {
		return context.getController();
	}

}
